const cp = require('child_process');
const path = require('path');
const {
  createMessageConnection,
  StreamMessageReader,
  StreamMessageWriter,
} = require('vscode-jsonrpc/node');

const projectRoot = path.join(__dirname, '..');
const serverPath = path.join('server', 'server.js');

// Start server as a child Node process
// Pass '--stdio' so the server uses stdio transport
const serverProcess = cp.spawn(process.execPath, [serverPath, '--stdio'], { cwd: projectRoot });

serverProcess.on('exit', (code: number) => {
  console.log('Server exited with code:', code);
});
serverProcess.stderr.on('data', (data: Buffer) => {
  console.error('[server stderr]', data.toString());
});

// Connect via stdio streams using JSON-RPC
const connection = createMessageConnection(
  new StreamMessageReader(serverProcess.stdout),
  new StreamMessageWriter(serverProcess.stdin)
);

connection.onNotification('textDocument/publishDiagnostics', (params: any) => {
  console.log('Diagnostics for', params.uri);
  console.log(JSON.stringify(params.diagnostics, null, 2));
  diagnosticsByUri[params.uri] = params.diagnostics;
});

connection.listen();

const uri = 'file:///demo.txt';
const diagnosticsByUri: Record<string, any[]> = {};
let currentText = 'Here is a TODO and another TODO.';
let version = 1;

async function delay(ms: number) {
  return new Promise((res) => setTimeout(res, ms));
}

function offsetAt(text: string, position: { line: number; character: number }): number {
  const lines = text.split('\n');
  let offset = 0;
  for (let i = 0; i < position.line; i++) {
    offset += lines[i].length + 1; // include newline
  }
  return offset + position.character;
}

function applyChanges(text: string, edits: Array<{ range: { start: any; end: any }; newText: string }>): string {
  // Apply from last to first by offset to avoid shifting
  const withOffsets = edits.map(e => ({
    start: offsetAt(text, e.range.start),
    end: offsetAt(text, e.range.end),
    newText: e.newText,
  })).sort((a, b) => b.start - a.start);
  let t = text;
  for (const e of withOffsets) {
    t = t.slice(0, e.start) + e.newText + t.slice(e.end);
  }
  return t;
}

function decodeSemanticTokens(data: number[], legend: { tokenTypes: string[]; tokenModifiers: string[] }) {
  const tokens: Array<{ line: number; character: number; length: number; type: string; modifiers: string[] }> = [];
  let line = 0;
  let char = 0;
  for (let i = 0; i < data.length; i += 5) {
    const deltaLine = data[i];
    const deltaStart = data[i + 1];
    const length = data[i + 2];
    const tokenType = data[i + 3];
    const tokenModBits = data[i + 4];
    line += deltaLine;
    char = deltaLine === 0 ? char + deltaStart : deltaStart;
    const type = legend.tokenTypes[tokenType] || String(tokenType);
    const modifiers: string[] = [];
    for (let b = 0; b < legend.tokenModifiers.length; b++) {
      if ((tokenModBits & (1 << b)) !== 0) {
        modifiers.push(legend.tokenModifiers[b]);
      }
    }
    tokens.push({ line, character: char, length, type, modifiers });
  }
  return tokens;
}

(async function main() {
  const initializeParams = {
    processId: process.pid,
    rootUri: null,
    capabilities: {},
  };

  const result: any = await connection.sendRequest('initialize', initializeParams);
  console.log('Server capabilities:', JSON.stringify(result.capabilities, null, 2));
  connection.sendNotification('initialized', {});

  // Open a document containing TODOs
  connection.sendNotification('textDocument/didOpen', {
    textDocument: {
      uri,
      languageId: 'plaintext',
      version,
      text: currentText,
    },
  });

  await delay(150);

  // Request completion at position inside the first TODO
  const pos = { line: 0, character: 12 };
  const completionParams = { textDocument: { uri }, position: pos, context: { triggerKind: 1 } };
  const items = await connection.sendRequest('textDocument/completion', completionParams);
  console.log('Completion items:', JSON.stringify(items, null, 2));

  // Request hover at the same position
  const hoverParams = { textDocument: { uri }, position: pos };
  const hover = await connection.sendRequest('textDocument/hover', hoverParams);
  console.log('Hover:', JSON.stringify(hover, null, 2));

  // Request definition at the same position
  const def = await connection.sendRequest('textDocument/definition', { textDocument: { uri }, position: pos });
  console.log('Definition:', JSON.stringify(def, null, 2));

  // Request references for the same symbol
  const refs = await connection.sendRequest('textDocument/references', { textDocument: { uri }, position: pos, context: { includeDeclaration: true } });
  console.log('References:', JSON.stringify(refs, null, 2));

  // Query workspace symbols
  const ws1 = await connection.sendRequest('workspace/symbol', { query: 'TODO' });
  console.log('Workspace symbols (TODO):', JSON.stringify(ws1, null, 2));
  const wsAll = await connection.sendRequest('workspace/symbol', { query: '' });
  console.log('Workspace symbols (all):', JSON.stringify(wsAll, null, 2));

  // Request Code Actions (quick fixes) on the first TODO
  const rangeTodo = { start: pos, end: { line: pos.line, character: pos.character + 4 } };
  const caParams = { textDocument: { uri }, range: rangeTodo, context: { diagnostics: diagnosticsByUri[uri] || [], only: ['quickfix'] } };
  const actions = await connection.sendRequest('textDocument/codeAction', caParams);
  console.log('Code actions:', JSON.stringify(actions, null, 2));

  // Apply the first quick fix edit if available
  const first = Array.isArray(actions) && actions.find((a: any) => a.edit && a.edit.changes && a.edit.changes[uri]);
  if (first) {
    const edits = first.edit.changes[uri];
    currentText = applyChanges(currentText, edits);
    version += 1;
    connection.sendNotification('textDocument/didChange', {
      textDocument: { uri, version },
      contentChanges: [{ text: currentText }],
    });
    console.log('Applied first quick fix; new text:', currentText);
  }

  await delay(150);

  // Add trailing spaces to demonstrate formatting
  currentText = currentText + '   ';
  version += 1;
  connection.sendNotification('textDocument/didChange', {
    textDocument: { uri, version },
    contentChanges: [{ text: currentText }],
  });
  console.log('Added trailing spaces to demonstrate formatting:', JSON.stringify(currentText));

  // Request full document formatting
  const fmtParams = { textDocument: { uri }, options: { tabSize: 2, insertSpaces: true } };
  const fmtEdits = await connection.sendRequest('textDocument/formatting', fmtParams);
  console.log('Formatting edits:', JSON.stringify(fmtEdits, null, 2));

  if (Array.isArray(fmtEdits) && fmtEdits.length > 0) {
    currentText = applyChanges(currentText, fmtEdits);
    version += 1;
    connection.sendNotification('textDocument/didChange', {
      textDocument: { uri, version },
      contentChanges: [{ text: currentText }],
    });
    console.log('Applied formatting; new text:', currentText);
  }

  await delay(150);

  // Request semantic tokens (full and range) and decode according to legend
  const legend = result?.capabilities?.semanticTokensProvider?.legend;
  if (legend) {
    const semFull = await connection.sendRequest('textDocument/semanticTokens/full', { textDocument: { uri } });
    console.log('Semantic tokens (raw full):', JSON.stringify(semFull, null, 2));
    if (semFull && Array.isArray(semFull.data)) {
      const decodedFull = decodeSemanticTokens(semFull.data, legend);
      console.log('Semantic tokens (decoded full):', JSON.stringify(decodedFull, null, 2));
    }

    const firstLineLength = currentText.split('\n')[0]?.length ?? 0;
    const semRange = await connection.sendRequest('textDocument/semanticTokens/range', {
      textDocument: { uri },
      range: { start: { line: 0, character: 0 }, end: { line: 0, character: firstLineLength } },
    });
    console.log('Semantic tokens (raw range):', JSON.stringify(semRange, null, 2));
    if (semRange && Array.isArray(semRange.data)) {
      const decodedRange = decodeSemanticTokens(semRange.data, legend);
      console.log('Semantic tokens (decoded range):', JSON.stringify(decodedRange, null, 2));
    }
  } else {
    console.log('No semantic tokens legend in server capabilities.');
  }

  await connection.sendRequest('shutdown');
  connection.sendNotification('exit');
  serverProcess.kill();
})();